package com.leo.train.business.service.Impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.leo.train.business.dto.DailyTrainTicketDto;
import com.leo.train.business.entity.DailyTrain;
import com.leo.train.business.entity.DailyTrainTicket;
import com.leo.train.business.entity.TrainInfo;
import com.leo.train.business.mapper.DailyTrainTicketMapper;
import com.leo.train.business.mapper.TrainInfoMapper;
import com.leo.train.business.service.DailySeatService;
import com.leo.train.business.service.DailyTrainTicketService;
import com.leo.train.common.enums.SeatTypeEnum;
import com.leo.train.common.enums.TrainTypeEnum;
import com.leo.train.common.exception.BusinessException;
import com.leo.train.common.exception.BusinessExceptionEnum;
import com.leo.train.common.resp.PageResult;
import com.leo.train.common.util.RedisUtil;
import com.leo.train.common.util.SnowUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class DailyTrainTicketServiceImpl implements DailyTrainTicketService {
    @Autowired
    private DailyTrainTicketMapper dailyTrainTicketMapper;
    @Autowired
    private TrainInfoMapper trainInfoMapper;
    @Autowired
    private DailySeatService dailySeatService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private RedissonClient redissonClient;
    
    // 布隆过滤器，用于防止缓存穿透
//    private BloomFilter<String> bloomFilter;
    
//    public DailyTrainTicketServiceImpl() {
//        // 初始化布隆过滤器，预计插入元素数量为10000，误判率为0.01
//        this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 10000, 0.01);
//    }

    @Override
    public void save(DailyTrainTicket dailyTrainTicket) {
        Long id = dailyTrainTicket.getId();
        if (id == null) {
            // 新增
            dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
            dailyTrainTicket.setCreateTime(new Date());
            dailyTrainTicket.setUpdateTime(new Date());
            dailyTrainTicketMapper.insert(dailyTrainTicket);
        } else {
            // 修改
            dailyTrainTicket.setUpdateTime(new Date());
            dailyTrainTicketMapper.update(dailyTrainTicket);
        }

    }

    /**
     * 余票列表查询 + 加入redis缓存
     * @param dailyTrainTicketDto
     * @return
     */
    @Override
    public PageResult<DailyTrainTicket> list(DailyTrainTicketDto dailyTrainTicketDto) {
        // 参数校验
        if (dailyTrainTicketDto.getDate() == null) {
            return new PageResult<>(0, Collections.emptyList());
        }
        String dateStr = DateUtil.formatDate(dailyTrainTicketDto.getDate());
        String redisKey = String.format("dailyTrainTicket:list:%s", dateStr);

        // 1. 使用布隆过滤器防穿透
//        if (bloomFilter != null && bloomFilter.mightContain(dateStr)) {
//            log.info("布隆过滤器拦截不存在日期: {}", dateStr);
//            return new PageResult<>(0, Collections.emptyList());
//        }

        // 2. 一级缓存：Redis
        List<DailyTrainTicket> redisValue = (List<DailyTrainTicket>) redisUtil.get(redisKey);
        // 去数据库里取数据，因数据库本身就没数据而造成缓存穿透，则返回空
        if (redisValue != null) {
            log.info("从redis中查询到余票列表，key：{}，value：{}", redisKey, redisValue);
            return new PageResult<>(redisValue.size(), redisValue);
        }

        // 3. 分布式锁防缓存击穿
        String lockKey = redisKey + ":lock";
        RLock lock = null;
        try {
            // 使用redisson，自带看门狗
            lock = redissonClient.getLock(lockKey);
            // 尝试获取锁，如果获取不到说明用户刚刚没有抢到锁
            // 锁续时间设置为30秒，锁自动续期
            boolean tryLock = lock.tryLock(0, 30, TimeUnit.SECONDS);
            if (!tryLock) {
                // 未获取到锁，等待并重试
                for (int i = 0; i < 3; i++) {
                    Thread.sleep(100); // 等待100ms
                    redisValue = (List<DailyTrainTicket>) redisUtil.get(redisKey);
                    if (redisValue != null) {
                        return new PageResult<>(redisValue.size(), redisValue);
                    }
                }
                // 所有重试都失败才抛异常
                throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
            }
            // 4. 获取到锁，查询数据库
            PageHelper.startPage(dailyTrainTicketDto.getPage(), dailyTrainTicketDto.getPageSize());
            Page<DailyTrainTicket> page = dailyTrainTicketMapper.pageQuery(dailyTrainTicketDto);
            List<DailyTrainTicket> result = page.getResult();

            // 5. 缓存空值防穿透
            if (CollUtil.isEmpty(result)) {
                // 缓存空值，设置较短过期时间，解决缓存穿透问题
                redisUtil.set(redisKey, Collections.emptyList(), 300); // 5分钟
//                if (bloomFilter != null) {
//                    bloomFilter.put(dateStr); // 加入布隆过滤器
//                }
            } else {
                // 正常缓存数据，设置随机过期时间防 缓存雪崩
                int expireTime = 60 * 60 * 24 + new Random().nextInt(1800); // 24小时 + 随机0-30分钟
                redisUtil.set(redisKey, result, expireTime);
            }
            // 6. 返回结果
            return new PageResult<>(page.getTotal(), result);

        } catch (Exception e) {
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
        } finally {
            // 释放锁
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

    }

    @Override
    public void delete(Long id) {
        dailyTrainTicketMapper.delete(id);
    }

    @Override
    public DailyTrainTicket detail(Long id) {
        DailyTrainTicket dailyTrainTicket = dailyTrainTicketMapper.detail(id);
        return dailyTrainTicket;
    }

    /**
     * 生成 某日某车次的 余票信息
     * @param dailyTrain 车次
     * @param date 日期
     * @param trainCode 车次编号
     * @return
     */
    @Transactional
    public void genDaily(DailyTrain dailyTrain, Date date, String trainCode) {
        log.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);
        Date now = new Date();
        // 1. 删除某日某车次的余票信息
        dailyTrainTicketMapper.deleteByTrainCodeAndDate(trainCode, date);
        // 2.查出某车次的所有的车站信息
        List<TrainInfo> trainInfoList = trainInfoMapper.selectByTrainCode(trainCode);
        if (CollUtil.isEmpty(trainInfoList)) {
            log.info("该车次没有车站基础数据，生成该车次的余票信息结束");
            return;
        }
        for (int i = 0; i < trainInfoList.size(); i++) {
            // 得到出发站
            TrainInfo trainInfoStart = trainInfoList.get(i);
            // 定义出发站的里程为0
            BigDecimal sumKm = BigDecimal.ZERO;
            for(int j = i + 1; j < trainInfoList.size(); j++) {
                // 得到到达站
                TrainInfo trainInfoEnd = trainInfoList.get(j);
                // 累加出发站到到达站的里程
                sumKm = sumKm.add(BigDecimal.valueOf(trainInfoEnd.getKm()));
                DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();
                dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainTicket.setDate(date);
                dailyTrainTicket.setTrainCode(trainCode);
                dailyTrainTicket.setStart(trainInfoStart.getName());
                dailyTrainTicket.setStartPinyin(trainInfoStart.getNamePinyin());
                dailyTrainTicket.setStartTime(DateUtil.parse(trainInfoStart.getOutTime())); // 出发时间为车次的出站时间
                dailyTrainTicket.setStartIndex(trainInfoStart.getIndex());
                dailyTrainTicket.setEnd(trainInfoEnd.getName());
                dailyTrainTicket.setEndPinyin(trainInfoEnd.getNamePinyin());
                dailyTrainTicket.setEndTime(DateUtil.parse(trainInfoEnd.getInTime())); // 终点站为车次的到达时间
                dailyTrainTicket.setEndIndex(trainInfoEnd.getIndex());
                // 计算出 不同座位类型的数量
                int ydz = dailySeatService.countSeat(date, trainCode, SeatTypeEnum.YDZ.getCode()); // 一等座数量
                int edz = dailySeatService.countSeat(date, trainCode, SeatTypeEnum.EDZ.getCode()); // 二等座数量
                int rw = dailySeatService.countSeat(date, trainCode, SeatTypeEnum.RW.getCode()); // 软卧数量
                int yw = dailySeatService.countSeat(date, trainCode, SeatTypeEnum.YW.getCode()); // 硬卧数量
                // 获取该车次的类型
                String trainType = dailyTrain.getType();
                TrainTypeEnum trainTypeEnum = TrainTypeEnum.fromCode(trainType);
                // 计算出 票价
                // 票价 = 里程之和 * 座位单价 * 车次类型的系数
                BigDecimal tdzPrice = sumKm.multiply(SeatTypeEnum.YDZ.getPrice()).multiply(trainTypeEnum.getPrice());
                BigDecimal edzPrice = sumKm.multiply(SeatTypeEnum.EDZ.getPrice()).multiply(trainTypeEnum.getPrice());
                BigDecimal rwPrice = sumKm.multiply(SeatTypeEnum.RW.getPrice()).multiply(trainTypeEnum.getPrice());
                BigDecimal ywPrice = sumKm.multiply(SeatTypeEnum.YW.getPrice()).multiply(trainTypeEnum.getPrice());
                dailyTrainTicket.setYdz(ydz);
                dailyTrainTicket.setYdzPrice(tdzPrice);
                dailyTrainTicket.setEdz(edz);
                dailyTrainTicket.setEdzPrice(edzPrice);
                dailyTrainTicket.setRw(rw);
                dailyTrainTicket.setRwPrice(rwPrice);
                dailyTrainTicket.setYw(yw);
                dailyTrainTicket.setYwPrice(ywPrice);
                dailyTrainTicket.setCreateTime(now);
                dailyTrainTicket.setUpdateTime(now);
                dailyTrainTicketMapper.insert(dailyTrainTicket);
            }
        }
        log.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);
    }

    /**
     * 定义按唯一键查询余票记录
     * @param trainCode 车次编号
     * @param date 日期
     * @param start 出发站
     * @param end 到达站
     * @return
     */
    public DailyTrainTicket selectByUnique(String trainCode, Date date, String start, String end) {
        return dailyTrainTicketMapper.selectByUnique(trainCode, date, start, end);
    }
}